{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.core.interactiveshell import InteractiveShell\n",
    "InteractiveShell.ast_node_interactivity = \"all\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 5、误差反向传播法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上一章中，我们介绍了神经网络的学习，并通过数值微分计算了神经网络的权重参数的梯度（严格来说，是损失函数关于权重参数的梯度）。  \n",
    "数值微分虽然简单，也容易实现，但缺点是计算上比较费时间。  \n",
    "本章我们将学习一个能够高效计算权重参数的梯度的方法——**误差反向传播法**。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要正确理解误差反向传播法，我个人认为有两种方法：一种是基于数学式；另一种是基于计算图（computational graph）。  \n",
    "前者是比较常见的方法，机器学习相关的图书中多数都是以数学式为中心展开论述的。  \n",
    "因为这种方法严密且简洁，所以确实非常合理，但如果一上来就围绕数学式进行探讨，会忽略一些根本的东西，止步于式子的罗列。  \n",
    "因此，本章希望大家通过计算图，直观地理解误差反向传播法。  \n",
    "然后，再结合实际的代码加深理解，相信大家一定会有种“原来如此！”的感觉。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.1、计算图"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "计算图将计算过程用图形表示出来。  \n",
    "这里说的图形是数据结构图，通过多个节点和边表示（连接节点的直线称为“边”）。  \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1.1、用计算图求解"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们尝试用计算图解简单的问题。下面我们要看的几个问题都是用心算就能解开的简单问题，这里的目的只是通过它们让大家熟悉计算图。  \n",
    "掌握了计算图的使用方法之后，在后面即将看到的复杂计算中它将发挥巨大威力，所以本节请一定学会计算图的使用方法。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**问题1**： 太郎在超市买了2个100日元一个的苹果，消费税是10%，请计算支付金额"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "计算图通过节点和箭头表示计算过程。节点用○表示，○中是计算的内容。  \n",
    "将计算的中间结果写在箭头的上方，表示各个节点的计算结果从左向右传递。  \n",
    "用计算图解问题1，求解过程如图5-1所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_1.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图5-1所示，开始时，苹果的100日元流到“× 2”节点，变成200日元，然后被传递给下一个节点。  \n",
    "接着，这个200日元流向“× 1.1”节点，变成220日元。  因此，从这个计算图的结果可知，答案为220日元。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "虽然图5-1中把“× 2”“× 1.1”等作为一个运算整体用○括起来了，不过只用○表示乘法运算“×”也是可行的。  \n",
    "此时，如图5-2所示，可以将“2”和“1.1”分别作为变量“苹果的个数”和“消费税”标在○外面。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_2.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "问题2： 太郎在超市买了2个苹果、 3个橘子。其中，苹果每个100日元，橘子每个150日元。  \n",
    "消费税是10%，请计算支付金额。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_3.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "综上，用计算图解题的情况下，需要按如下流程进行：  \n",
    "1.构建计算图。  \n",
    "2.在计算图上，从左向右进行计算。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里的第2歩“从左向右进行计算”是一种正方向上的传播，简称为**正向传播**（forward propagation）。  \n",
    "正向传播是从计算图出发点到结束点的传播。  \n",
    "既然有正向传播这个名称，当然也可以考虑反向（从图上看的话，就是从右向左）的传播。  \n",
    "实际上，这种传播称为**反向传播**（backward propagation）。  \n",
    "反向传播将在接下来的导数计算中发挥重要作用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1.2、局部计算"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "计算图的特征是可以通过传递“局部计算”获得最终结果。  \n",
    "“局部”这个词的意思是“与自己相关的某个小范围”。  \n",
    "局部计算是指，无论全局发生了什么，都能只根据与自己相关的信息输出接下来的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们用一个具体的例子来说明局部计算。比如，在超市买了2个苹果和\n",
    "其他很多东西。此时，可以画出如图5-4所示的计算图。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_4.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图5-4所示，假设（经过复杂的计算）购买的其他很多东西总共花费4000日元。  \n",
    "这里的重点是，各个节点处的计算都是局部计算。  \n",
    "这意味着，例如苹果和其他很多东西的求和运算（4000 + 200 → 4200）并不关心4000这个\n",
    "数字是如何计算而来的，只要把两个数字相加就可以了。  \n",
    "换言之，各个节点处只需进行与自己有关的计算（在这个例子中是对输入的两个数字进行加法\n",
    "运算），不用考虑全局。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "综上，计算图可以集中精力于局部计算。  \n",
    "无论全局的计算有多么复杂，各个步骤所要做的就是对象节点的局部计算。  \n",
    "虽然局部计算非常简单，但是通过传递它的计算结果，可以获得全局的复杂计算的结果。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "比如，组装汽车是一个复杂的工作，通常需要进行“流水线”作业。  \n",
    "每个工人（机器）所承担的都是被简化了的工作，这个工作的成果会传递给下一个工人，直至汽车组装完成。  \n",
    "计算图将复杂的计算分割成简单的局部计算，和流水线作业一样，将局部计算的结果传递给\n",
    "下一个节点。  \n",
    "在将复杂的计算分解成简单的计算这一点上与汽车的组装有相似之处。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1.3、为何用计算图解题"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面我们用计算图解答了两个问题，那么计算图到底有什么优点呢？  \n",
    "一个优点就在于前面所说的局部计算。  \n",
    "无论全局是多么复杂的计算，都可以通过局部计算使各个节点致力于简单的计算，从而简化问题。  \n",
    "另一个优点是，利用计算图可以将中间的计算结果全部保存起来（比如，计算进行到2个苹果时的金额是200日元、加上消费税之前的金额650日元等）。  \n",
    "但是只有这些理由可能还无法令人信服。  \n",
    "实际上，使用计算图最大的原因是，可以通过反向传播高效计算导数。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在介绍计算图的反向传播时，我们再来思考一下问题1。  \n",
    "问题1中，我们计算了购买2个苹果时加上消费税最终需要支付的金额。  \n",
    "这里，假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额，即求“支付金\n",
    "额关于苹果的价格的导数”。  \n",
    "设苹果的价格为x，支付金额为L，则相当于求$$\n",
    "\\frac { \\partial L } { \\partial x }\n",
    "$$。  \n",
    "这个导数的值表示当苹果的价格稍微上涨时，支付金额会增加多少。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如前所述，“支付金额关于苹果的价格的导数”的值可以通过计算图的反向传播求出来。  \n",
    "先来看一下结果，如图5-5所示，可以通过计算图的反向传播求导数（关于如何进行反向传播，接下来马上会介绍）。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_5.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图5-5所示，反向传播使用与正方向相反的箭头（粗线）表示。  \n",
    "反向传播传递“局部导数”，将导数的值写在箭头的下方。  \n",
    "在这个例子中，反向传播从右向左传递导数的值（1 → 1.1 → 2.2）。  \n",
    "从这个结果中可知，“支付金额关于苹果的价格的导数”的值是2.2。  \n",
    "这意味着，如果苹果的价格上涨1日元，最终的支付金额会增加2.2日元（严格地讲，如果苹果的价格增加某个微小值，则最终的支付金额将增加那个微小值的2.2倍）。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里只求了关于苹果的价格的导数，不过“支付金额关于消费税的导数”“支付金额关于苹果的个数的导数”等也都可以用同样的方式算出来。  \n",
    "并且，计算中途求得的导数的结果（中间传递的导数）可以被共享，从而可以高效地计算多个导数。  \n",
    "综上，计算图的优点是，可以通过正向传播和反向传播高效地计算各个变量的导数值。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.2、链式法则"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "反向传播将局部导数向正方向的反方向（从右到左）传递，一开始可能会让人感到困惑。  \n",
    "传递这个局部导数的原理，是基于链式法则（chain rule）的。  \n",
    "本节将介绍链式法则，并阐明它是如何对应计算图上的反向传播的。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.1、计算图的反向传播"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们先来看一个使用计算图的反向传播的例子。  \n",
    "假设存在y = f(x)的计算，这个计算的反向传播如图5-6所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_6.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图所示，反向传播的计算顺序是，将信号E乘以节点的局部导数$$\n",
    "\\left( \\frac { \\partial y } { \\partial x } \\right)\n",
    "$$，然后将结果传递给下一个节点。  \n",
    "这里所说的局部导数是指正向传播中y = f(x)的导数，也就是y关于x的导数$$\n",
    "\\left( \\frac { \\partial y } { \\partial x } \\right)\n",
    "$$。比如，假设$$\n",
    "y = f ( x ) = x ^ { 2 }\n",
    "$$，\n",
    "则局部导数为$$\n",
    "\\frac { \\partial y } { \\partial x } = 2 x\n",
    "$$。把这个局部导数乘以上游传过来的值（本例中为E），然后传递给前面的节点。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.2、什么是链式法则"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "介绍链式法则时，我们需要先从**复合函数**说起。复合函数是由多个函数构成的函数。  \n",
    "比如，$$\n",
    "z = ( x + y ) ^ { 2 }\n",
    "$$是由式（5.1）所示的两个式子构成的。\n",
    "$$\n",
    "\\begin{array} { c } { z = t ^ { 2 } } \\\\ { t = x + y } \\end{array}\n",
    "$$\n",
    "<center>式（5.1）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "链式法则是关于复合函数的导数的性质，定义如下：  \n",
    "`如果某个函数由复合函数表示，则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是链式法则的原理，乍一看可能比较难理解，但实际上它是一个\n",
    "非常简单的性质。以式（5.1）为例， $$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$（z关于x的导数）可以用 （z关于t\n",
    "的导数）和 $$\n",
    "\\frac { \\partial z } { \\partial t }\n",
    "$$（t关于x的导数）的乘积表示。用数学式表示的话，可以写成\n",
    "式（5.2）。$$\n",
    "\\frac { \\partial z } { \\partial x } = \\frac { \\partial z } { \\partial t } \\frac { \\partial t } { \\partial x }\n",
    "$$\n",
    "<center>式（5.2）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "式（5.2）中的∂t正好可以像下面这样“互相抵消”，所以记起来很简单。   \n",
    "  \n",
    "  \n",
    "  \n",
    "现在我们使用链式法则，试着求式（5.2）的导数 。  \n",
    "为此，我们要先求式（5.1）中的局部导数（偏导数）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\begin{array} { l } { \\frac { \\partial z } { \\partial t } = 2 t } \\\\ { \\frac { \\partial t } { \\partial x } = 1 } \\end{array}\n",
    "$$  \n",
    "<center>式（5.3）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这是基于导数公式的解析解。  \n",
    "然后，最后要计算的$$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$可由式（5.3）求得的导数的乘积计算出来。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\frac { \\partial z } { \\partial x } = \\frac { \\partial z } { \\partial t } \\frac { \\partial t } { \\partial x } = 2 t \\cdot 1 = 2 ( x + y )\n",
    "$$\n",
    "<center>式（5.4）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2.3、链式法则和计算图"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们尝试将式（5.4）的链式法则的计算用计算图表示出来。  \n",
    "如果用“**2”节点表示平方运算的话，则计算图如图5-7所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_7.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图所示，计算图的反向传播从右到左传播信号。  \n",
    "反向传播的计算顺序是，先将节点的输入信号乘以节点的局部导数（偏导数），然后再传递给下一个节点。  \n",
    "比如，反向传播时，“\\**2”节点的输入是$$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$ ，将其乘以局部导数$$\n",
    "\\frac { \\partial z } { \\partial t }\n",
    "$$（因为正向传播时输入是t、输出是z，所以这个节点的局部导数是 $$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$），然后传递给下一个节点。  \n",
    "另外，图5-7中反向传播最开始的信号 在前面的数学式中没有出现，这是因为$$\n",
    "\\frac { \\partial z } { \\partial z } = 1\n",
    "$$ ，所以在刚才的式子中被省略了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图 5-7 中需要注意的是最左边的反向传播的结果。  \n",
    "根据链式法则，$$\n",
    "\\frac { \\partial z } { \\partial z } \\frac { \\partial z } { \\partial t } \\frac { \\partial t } { \\partial x } = \\frac { \\partial z } { \\partial t } \\frac { \\partial t } { \\partial x } = \\frac { \\partial z } { \\partial x }\n",
    "$$成立，对应“z关于x的导数”。  \n",
    "也就是说，反向传播是基于链式法则的。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "把式（5.3）的结果代入到图 5-7 中，结果如图 5-8 所示， $$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$的结果为\n",
    "2(x + y)。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_8.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.3、反向传播"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上一节介绍了计算图的反向传播是基于链式法则成立的。  \n",
    "本节将以“+”和“×”等运算为例，介绍反向传播的结构。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3.1、加法节点的反向传播"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先来考虑加法节点的反向传播。  \n",
    "这里以z = x + y为对象，观察它的反向传播。   \n",
    "z = x + y的导数可由下式（解析性地）计算出来。  \n",
    "$$\n",
    "\\begin{array} { l } { \\frac { \\partial z } { \\partial x } = 1 } \\\\ { \\frac { \\partial z } { \\partial y } = 1 } \\end{array}\n",
    "$$  \n",
    "<center>式（5.5）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如式（5.5）所示， $$\n",
    "\\frac { \\partial z } { \\partial x }\n",
    "$$和$$\n",
    "\\frac { \\partial z } { \\partial y }\n",
    "$$同时都等于1。  \n",
    "因此，用计算图表示的话，如图5-9所示。  \n",
    "在图5-9中，反向传播将从上游传过来的导数（本例中是$$\n",
    "\\frac { \\partial L } { \\partial z }\n",
    "$$ ）乘以1，然后传向下游。  \n",
    "也就是说，因为加法节点的反向传播只乘以1，所以输入的值会原封不动地流向下一个节点。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_9.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另外，本例中把从上游传过来的导数的值设为$$\n",
    "\\frac { \\partial L } { \\partial z }\n",
    "$$。  \n",
    "这是因为，如图5-10所示，我们假定了一个最终输出值为L的大型计算图。   \n",
    "z = x + y的计算位于这个大型计算图的某个地方，从上游会传来$$\n",
    "\\frac { \\partial I } { \\partial z }\n",
    "$$的值，并向下游传递$$\n",
    "\\frac { \\partial L } { \\partial x }\n",
    "$$和$$\n",
    "\\frac { \\partial L } { \\partial y }\n",
    "$$。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_10.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在来看一个加法的反向传播的具体例子。  \n",
    "假设有“10 + 5=15”这一计算，反向传播时，从上游会传来值1.3。  \n",
    "用计算图表示的话，如图5-11所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_11.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为加法节点的反向传播只是将输入信号输出到下一个节点，所以如图5-11所示，反向传播将1.3向下一个节点传递"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3.2、乘法节点的反向传播"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，我们看一下乘法节点的反向传播。  \n",
    "这里我们考虑z = xy。  \n",
    "这个式子的导数用式（5.6）表示：  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\begin{array} { l } { \\frac { \\partial z } { \\partial x } = y } \\\\ { \\frac { \\partial z } { \\partial y } = x } \\end{array}\n",
    "$$\n",
    "<center>式（5.6）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据式（5.6），可以像图5-12那样画计算图。  \n",
    "乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。  \n",
    "翻转值表示一种翻转关系，如图5-12所示，正向传播时信号是x的话，反向传播时则是y；正向传播时信号是y的话，反向传播时则是x。  \n",
    "现在我们来看一个具体的例子。  \n",
    "比如，假设有“10 × 5 = 50”这一计算，反向传播时，从上游会传来值1.3。  \n",
    "用计算图表示的话，如图5-13所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_12_5_13.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为乘法的反向传播会乘以输入信号的翻转值，所以各自可按1.3 × 5 = 6.5、 1.3 × 10 = 13计算。  \n",
    "另外，加法的反向传播只是将上游的值传给下游，并不需要正向传播的输入信号。  \n",
    "但是，乘法的反向传播需要正向传播时的输入信号值。  \n",
    "因此，实现乘法节点的反向传播时，要保存正向传播的输入信号。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3.3、苹果的例子"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再来思考一下本章最开始举的购买苹果的例子（2个苹果和消费税）。  \n",
    "这里要解的问题是苹果的价格、苹果的个数、消费税这3个变量各自如何影响最终支付的金额。  \n",
    "这个问题相当于求“支付金额关于苹果的价格的导数”“支付金额关于苹果的个数的导数”“支付金额关于消费税的导数”。  \n",
    "用计算图的反向传播来解的话，求解过程如图5-14所示："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_14.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如前所述，乘法节点的反向传播会将输入信号翻转后传给下游。  \n",
    "从图5-14的结果可知，苹果的价格的导数是2.2，苹果的个数的导数是110，消费税的导数是200。  \n",
    "这可以解释为，如果消费税和苹果的价格增加相同的值，则消费税将对最终价格产生200倍大小的影响，苹果的价格将产生2.2倍大小的影响。  \n",
    "不过，因为这个例子中消费税和苹果的价格的量纲不同，所以才形成了这样的结果（消费税的1是100%，苹果的价格的1是1日元）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.4、简单层的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本节将用Python实现前面的购买苹果的例子。  \n",
    "这里，我们把要实现的计算图的乘法节点称为“乘法层”（MulLayer），加法节点称为“加法层”（AddLayer）。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下一节，我们将把构建神经网络的“层”实现为一个类。  \n",
    "这里所说的“层”是神经网络中功能的单位。  \n",
    "比如，负责 sigmoid 函数的Sigmoid、负责矩阵乘积的Affine等，都以层为单位进行实现。  \n",
    "因此，这里也以层为单位来实现乘法节点和加法节点。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.4.1、乘法层的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "层的实现中有两个共通的方法（接口） forward()和backward()。   \n",
    "forward()对应正向传播， backward()对应反向传播。  \n",
    "现在来实现乘法层。  \n",
    "乘法层作为 MulLayer类，其实现过程如下所示。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MulLayer:\n",
    "    def __init__(self):\n",
    "        self.x = None\n",
    "        self.y = None\n",
    "    def forward(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        out = x * y\n",
    "        return out\n",
    "    def backward(self, dout):\n",
    "        dx = dout * self.y # 翻转x和y\n",
    "        dy = dout * self.x\n",
    "        return dx, dy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\\_\\_init__()中会初始化实例变量x和y，它们用于保存正向传播时的输入值。  \n",
    "forward()接收 x和 y两个参数，将它们相乘后输出。  \n",
    "backward()将从上游传来的导数（dout）乘以正向传播的翻转值，然后传给下游。  \n",
    "上面就是 MulLayer的实现。  \n",
    "现在我们使用 MulLayer实现前面的购买苹果的例子（2个苹果和消费税）。  \n",
    "上一节中我们使用计算图的正向传播和反向传播，像图5-16这样进行了计算。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_16.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用这个乘法层的话，图5-16的正向传播可以像下面这样实现:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "220.00000000000003\n"
     ]
    }
   ],
   "source": [
    "apple = 100\n",
    "apple_num = 2\n",
    "tax = 1.1\n",
    "# layer\n",
    "mul_apple_layer = MulLayer()\n",
    "mul_tax_layer = MulLayer()\n",
    "# forward\n",
    "apple_price = mul_apple_layer.forward(apple, apple_num)\n",
    "price = mul_tax_layer.forward(apple_price, tax)\n",
    "print(price) # 220"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "此外，关于各个变量的导数可由backward()求出。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.2 110.00000000000001 200\n"
     ]
    }
   ],
   "source": [
    "# backward\n",
    "dprice = 1\n",
    "dapple_price, dtax = mul_tax_layer.backward(dprice)\n",
    "dapple, dapple_num = mul_apple_layer.backward(dapple_price)\n",
    "print(dapple, dapple_num, dtax) # 2.2 110 200"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里，调用backward()的顺序与调用forward()的顺序相反。  \n",
    "此外，要注意backward()的参数中需要输入“关于正向传播时的输出变量的导数”。  \n",
    "比如，mul_apple_layer乘法层在正向传播时会输出apple_price，在反向传播时，则会将apple_price的导数dapple_price设为参数。  \n",
    "另外，这个程序的运行结果和图5-16是一致的。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.4.2、加法层的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，我们实现加法节点的加法层，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class AddLayer:\n",
    "    def __init__(self):\n",
    "        pass\n",
    "    def forward(self, x, y):\n",
    "        out = x + y\n",
    "        return out\n",
    "    def backward(self, dout):\n",
    "        dx = dout * 1\n",
    "        dy = dout * 1\n",
    "        return dx, dy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "加法层不需要特意进行初始化，所以 __init__()中什么也不运行（pass\n",
    "语句表示“什么也不运行”）。  \n",
    "加法层的 forward()接收 x和 y两个参数，将它们相加后输出。   \n",
    "backward()将上游传来的导数（dout）原封不动地传递给下游。  \n",
    "现在，我们使用加法层和乘法层，实现图5-17所示的购买2个苹果和3个橘子的例子。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_17.png\"></img>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "715.0000000000001\n",
      "110.00000000000001 2.2 3.3000000000000003 165.0 650\n"
     ]
    }
   ],
   "source": [
    "apple = 100\n",
    "apple_num = 2\n",
    "orange = 150\n",
    "orange_num = 3\n",
    "tax = 1.1\n",
    "\n",
    "# layer\n",
    "mul_apple_layer = MulLayer()\n",
    "mul_orange_layer = MulLayer()\n",
    "add_apple_orange_layer = AddLayer()\n",
    "mul_tax_layer = MulLayer()\n",
    "\n",
    "# forward\n",
    "apple_price = mul_apple_layer.forward(apple, apple_num) #(1)\n",
    "orange_price = mul_orange_layer.forward(orange, orange_num) #(2)\n",
    "all_price = add_apple_orange_layer.forward(apple_price, orange_price) #(3)\n",
    "price = mul_tax_layer.forward(all_price, tax) #(4)\n",
    "\n",
    "# backward\n",
    "dprice = 1\n",
    "dall_price, dtax = mul_tax_layer.backward(dprice) #(4)\n",
    "dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) #(3)\n",
    "dorange, dorange_num = mul_orange_layer.backward(dorange_price) #(2)\n",
    "dapple, dapple_num = mul_apple_layer.backward(dapple_price) #(1)\n",
    "\n",
    "print(price) # 715\n",
    "print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个实现稍微有一点长，但是每一条命令都很简单。  \n",
    "首先，生成必要的层，以合适的顺序调用正向传播的 forward()方法。  \n",
    "然后，用与正向传播相反的顺序调用反向传播的backward()方法，就可以求出想要的导数。  \n",
    "综上，计算图中层的实现（这里是加法层和乘法层）非常简单，使用这些层可以进行复杂的导数计算。  \n",
    "下面，我们来实现神经网络中使用的层。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.5、激活函数层的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，我们将计算图的思路应用到神经网络中。  \n",
    "这里，我们把构成神经网络的层实现为一个类。  \n",
    "先来实现激活函数的ReLU层和Sigmoid层。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.5.1、ReLU层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "激活函数ReLU（Rectified Linear Unit）由下式（5.7）表示："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "y = \\left\\{ \\begin{array} { l l } { x } & { ( x > 0 ) } \\\\ { 0 } & { ( x \\leqslant 0 ) } \\end{array} \\right.\n",
    "$$\n",
    "<center>式（5.7）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过式（5.7），可以求出y关于x的导数，如式（5.8）所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\frac { \\partial y } { \\partial x } = \\left\\{ \\begin{array} { l l } { 1 } & { ( x > 0 ) } \\\\ { 0 } & { ( x \\leqslant 0 ) } \\end{array} \\right.\n",
    "$$ <center>式（5.8）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在式（5.8）中，如果正向传播时的输入x大于0，则反向传播会将上游的值原封不动地传给下游。  \n",
    "反过来，如果正向传播时的x小于等于0，则反向传播中传给下游的信号将停在此处。  \n",
    "用计算图表示的话，如图5-18所示。  \n",
    "现在我们来实现ReLU层。  \n",
    "在神经网络的层的实现中，一般假定forward()和backward()的参数是NumPy数组。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Relu:\n",
    "    def __init__(self):\n",
    "        self.mask = None\n",
    "    def forward(self, x):\n",
    "        self.mask = (x <= 0)\n",
    "        out = x.copy()\n",
    "        out[self.mask] = 0\n",
    "        return out\n",
    "    def backward(self, dout):\n",
    "        dout[self.mask] = 0\n",
    "        dx = dout\n",
    "        return dx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Relu类有实例变量mask。  \n",
    "这个变量mask是由True/False构成的NumPy数组，它会把正向传播时的输入x的元素中小于等于0的地方保存为True，其他地方（大于0的元素）保存为 False。  \n",
    "如下例所示， mask变量保存了由 True/False构成的NumPy数组。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1. , -0.5],\n",
       "       [-2. ,  3. ]])"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x = np.array( [[1.0, -0.5], [-2.0, 3.0]] )\n",
    "x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[False,  True],\n",
       "       [ True, False]])"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mask=(x<=0)\n",
    "mask"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如图5-18所示，如果正向传播时的输入值小于等于0，则反向传播的值为0。  \n",
    "因此，反向传播中会使用正向传播时保存的 mask，将从上游传来的 dout的mask中的元素为True的地方设为0。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_18.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ReLU 层的作用就像电路中的开关一样。  \n",
    "正向传播时，有电流通过的话，就将开关设为ON；没有电流通过的话，就将开关设为OFF。  \n",
    "反向传播时，开关为ON的话，电流会直接通过；开关为OFF的话，则不会有电流通过。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.5.2、Sigmoid层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，我们来实现sigmoid函数。  \n",
    "sigmoid函数由式（5.9）表示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "y = \\frac { 1 } { 1 + \\exp ( - x ) }\n",
    "$$ \n",
    "<center>式（5.9）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用计算图表示式（5.9）的话，则如图5-19所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_19.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图5-19中，除了“×”和“+”节点外，还出现了新的“exp”和“/”节点。  \n",
    "“exp”节点会进行y = exp(x)的计算，“/”节点会进行 的计算。  \n",
    "如图5-19所示，式（5.9）的计算由局部计算的传播构成。  \n",
    "下面我们就来进行图5-19的计算图的反向传播。  \n",
    "这里，作为总结，我们来依次看一下反向传播的流程。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"/\"节点表示 ，它的导数可以解析性地表示为下式:\n",
    "$$\n",
    "\\begin{aligned} \\frac { \\partial y } { \\partial x } & = - \\frac { 1 } { x ^ { 2 } } \\\\ & = - y ^ { 2 } \\end{aligned}\n",
    "$$\n",
    "<center>式（5.10）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据式（5.10），反向传播时，会将上游的值乘以$$\n",
    "- y ^ { 2 }\n",
    "$$（正向传播的输出的\n",
    "平方乘以−1后的值）后，再传给下游。  计算图如下所示:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/sigmoid_step1.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "“+”节点将上游的值原封不动地传给下游。  \n",
    "计算图如下所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/sigmoid_step2.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "exp”节点表示y = exp(x)，它的导数由下式表示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\frac { \\partial y } { \\partial x } = \\exp ( x )\n",
    "$$\n",
    "<center>式（5.11）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "计算图中，上游的值乘以正向传播时的输出（这个例子中是exp(−x)）后，再传给下游。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/sigmoid_step3.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "“×”节点将正向传播时的值翻转后做乘法运算。  \n",
    "因此，这里要乘以−1。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_20.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据上述内容，图5-20的计算图可以进行Sigmoid层的反向传播。  \n",
    "从图5-20的结果可知，反向传播的输出为$$\n",
    "\\frac { \\partial L } { \\partial y } y ^ { 2 } \\exp ( - x )\n",
    "$$，这个值会传播给下游的节点。  \n",
    "这里要注意， $$\n",
    "\\frac { \\partial L } { \\partial y } y ^ { 2 } \\exp ( - x )\n",
    "$$这个值只根据正向传播时的输入x和输出y就可以算出来。  \n",
    "因此，图5-20的计算图可以画成图5-21的集约化的“sigmoid”节点。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_21.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图5-20的计算图和简洁版的图5-21的计算图的计算结果是相同的，但是，简洁版的计算图可以省略反向传播中的计算过程，因此计算效率更高。  \n",
    "此外，通过对节点进行集约化，可以不用在意Sigmoid层中琐碎的细节，而只需要专注它的输入和输出，这一点也很重要。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另外， $$\n",
    "\\frac { \\partial L } { \\partial y } y ^ { 2 } \\exp ( - x )\n",
    "$$可以进一步整理如下:  \n",
    "$$\n",
    "\\begin{aligned} \\frac { \\partial L } { \\partial y } y ^ { 2 } \\exp ( - x ) & = \\frac { \\partial L } { \\partial y } \\frac { 1 } { ( 1 + \\exp ( - x ) ) ^ { 2 } } \\exp ( - x ) \\\\ & = \\frac { \\partial L } { \\partial y } \\frac { 1 } { 1 + \\exp ( - x ) } \\frac { \\exp ( - x ) } { 1 + \\exp ( - x ) } \\\\ & = \\frac { \\partial L } { \\partial y } y ( 1 - y ) \\end{aligned}\n",
    "$$  \n",
    "<center>式（5.12）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因此，图5-21所表示的Sigmoid层的反向传播，只根据正向传播的输出就能计算出来。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_22.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，我们用Python实现Sigmoid层。  \n",
    "参考图5-22，可以像下面这样实现:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Sigmoid:\n",
    "    def __init__(self):\n",
    "        self.out = None\n",
    "    def forward(self, x):\n",
    "        out = 1 / (1 + np.exp(-x))\n",
    "        self.out = out\n",
    "        return out\n",
    "    def backward(self, dout):\n",
    "        dx = dout * (1.0 - self.out) * self.out\n",
    "        return dx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个实现中，正向传播时将输出保存在了实例变量 out中。  \n",
    "然后，反向传播时，使用该变量out进行计算。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.6、Affine/Softmax层的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.6.1、Affine层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "神经网络的正向传播中，为了计算加权信号的总和，使用了矩阵的乘积运算（NumPy中是 np.dot()，具体请参照3.3节）。  \n",
    "比如，还记得我们用Python进行了下面的实现吗？  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(2,)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "text/plain": [
       "(2, 3)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "text/plain": [
       "(3,)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = np.random.rand(2) # 输入\n",
    "W = np.random.rand(2,3) # 权重\n",
    "B = np.random.rand(3) # 偏置  \n",
    "X.shape # (2,)\n",
    "W.shape # (2, 3)\n",
    "B.shape # (3,)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "Y = np.dot(X, W) + B"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里， X、 W、 B 分别是形状为(2,)、 (2, 3)、 (3,)的多维数组。  \n",
    "这样一来，神经元的加权和可以用 Y = np.dot(X, W) + B计算出来。  \n",
    "然后， Y 经过激活函数转换后，传递给下一层。  \n",
    "这就是神经网络正向传播的流程。  \n",
    "此外，我们来复习一下，矩阵的乘积运算的要点是使对应维度的元素个数一致。  \n",
    "比如，如下面的图5-23所示， X和W 的乘积必须使对应维度的元素个数一致。  \n",
    "另外，这里矩阵的形状用(2, 3)这样的括号表示（为了和NumPy的 shape属性的输出一致）。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_23.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”。  \n",
    "几何中，仿射变换包括一次线性变换和一次平移，分别对应神经网络的加权和运算与加偏置运算。  \n",
    "因此，这里将进行仿射变换的处理实现为“Affine层”。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_24.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图5-24是比较简单的计算图，不过要注意X、 W、 B是矩阵（多维数组）。  \n",
    "之前我们见到的计算图中各个节点间流动的是标量，而这个例子中各个节点间传播的是矩阵。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们来考虑图5-24的计算图的反向传播。  \n",
    "以矩阵为对象的反向传播，按矩阵的各个元素进行计算时，步骤和以标量为对象的计算图相同。  \n",
    "实际写一下的话，可以得到下式（这里省略了式（5.13）的推导过程）。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\begin{aligned} \\frac { \\partial L } { \\partial \\boldsymbol { X } } & = \\frac { \\partial L } { \\partial \\boldsymbol { Y } } \\cdot \\boldsymbol { W } ^ { \\mathrm { T } } \\\\ \\frac { \\partial L } { \\partial \\boldsymbol { W } } & = \\boldsymbol { X } ^ { \\mathrm { T } } \\cdot \\frac { \\partial L } { \\partial \\boldsymbol { Y } } \\end{aligned}\n",
    "$$  \n",
    "<center>式（5.13）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "式（5.13）中W T的T表示转置。转置操作会把W的元素(i, j)换成元素(j, i)。  \n",
    "用数学式表示的话，可以写成下面这样:$$\n",
    "\\begin{array} { c } { \\boldsymbol { W } ^ { \\mathrm { T } } = \\left( \\begin{array} { c c c } { w _ { 11 } } & { w _ { 12 } } & { w _ { 13 } } \\\\ { w _ { 21 } } & { w _ { 22 } } & { w _ { 23 } } \\end{array} \\right) } \\\\ { \\boldsymbol { W } ^ { \\mathrm { T } } = \\left( \\begin{array} { c c } { w _ { 11 } } & { w _ { 21 } } \\\\ { w _ { 12 } } & { w _ { 22 } } \\\\ { w _ { 13 } } & { w _ { 23 } } \\end{array} \\right) } \\end{array}\n",
    "$$<center>式（5.14）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如式（5.14）所示，如果W的形状是(2, 3)， WT的形状就是(3, 2)。  \n",
    "现在，我们根据式（5.13），尝试写出计算图的反向传播，如图5-25所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_25.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们看一下图2-25的计算图中各个变量的形状。尤其要注意， X和$$\n",
    "\\frac { \\partial L } { \\partial \\boldsymbol { X } }\n",
    "$$形状相同， W和$$\n",
    "\\frac { \\partial L } { \\partial \\boldsymbol { W } }\n",
    "$$形状相同。从下面的数学式可以很明确地看出X和$$\n",
    "\\frac { \\partial L } { \\partial \\boldsymbol { X } }\n",
    "$$形状相同。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\begin{array} { l } { \\boldsymbol { X } = \\left( x _ { 0 } , x _ { 1 } , \\cdots , x _ { n } \\right) } \\\\ { \\frac { \\partial L } { \\partial \\boldsymbol { X } } = \\left( \\frac { \\partial L } { \\partial x _ { 0 } } , \\frac { \\partial L } { \\partial x _ { 1 } } , \\cdots , \\frac { \\partial L } { \\partial x _ { n } } \\right) } \\end{array}\n",
    "$$\n",
    "<center>式（5.15）</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为什么要注意矩阵的形状呢？  \n",
    "因为矩阵的乘积运算要求对应维度的元素个数保持一致，通过确认一致性，就可以导出式（5.13）。  \n",
    "比如， $$\n",
    "\\frac { \\partial L } { \\partial \\mathbf { Y } }\n",
    "$$的形状是(3,)， W的形状是(2, 3)时，思考 和W T的乘积，使得$$\n",
    "\\frac { \\partial L } { \\partial \\boldsymbol { X } }\n",
    "$$的形状为(2,)\n",
    "（图5-26）。这样一来，就会自然而然地推导出式（5.13）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_26.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.6.2、批版本的Affine层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面介绍的Affine层的输入X是以单个数据为对象的。  \n",
    "现在我们考虑N个数据一起进行正向传播的情况，也就是批版本的Affine层。  \n",
    "先给出批版本的Affine层的计算图，如图5-27所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_27.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "与刚刚不同的是，现在输入X的形状是(N, 2)。  \n",
    "之后就和前面一样，在计算图上进行单纯的矩阵计算。  \n",
    "反向传播时，如果注意矩阵的形状，就可以和前面一样推导出 $$\n",
    "\\frac { \\partial L } { \\partial X }\n",
    "$$和 $$\n",
    "\\frac { \\partial L } { \\partial \\boldsymbol { W } }\n",
    "$$。  \n",
    "加上偏置时，需要特别注意。  \n",
    "正向传播时，偏置被加到X· W的各个数据上。  \n",
    "比如， N = 2（数据为2个）时，偏置会被分别加到这2个数据（各自的计算结果）上，具体的例子如下所示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 0,  0,  0],\n",
       "       [10, 10, 10]])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])\n",
    "B = np.array([1, 2, 3])\n",
    "X_dot_W"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1,  2,  3],\n",
       "       [11, 12, 13]])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X_dot_W + B"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "正向传播时，偏置会被加到每一个数据（第1个、第2个……）上。  \n",
    "因此，反向传播时，各个数据的反向传播的值需要汇总为偏置的元素。  \n",
    "用代码表示的话，如下所示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1, 2, 3],\n",
       "       [4, 5, 6]])"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dY = np.array([[1, 2, 3,], [4, 5, 6]])\n",
    "dY"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([5, 7, 9])"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dB = np.sum(dY, axis=0)\n",
    "dB"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个例子中，假定数据有2个（N = 2）。  \n",
    "偏置的反向传播会对这2个数据的导数按元素进行求和。  \n",
    "因此，这里使用了 np.sum()对第0轴（以数据为单位的轴， axis=0）方向上的元素进行求和。  \n",
    "综上所述， Affine的实现如下所示。  \n",
    "另外， common/layers.py中的Affine的实现考虑了输入数据为张量（四维数据）的情况，与这里介绍的稍有差别。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Affine:\n",
    "    def __init__(self, W, b):\n",
    "        self.W = W\n",
    "        self.b = b\n",
    "        self.x = None\n",
    "        self.dW = None\n",
    "        self.db = None\n",
    "    def forward(self, x):\n",
    "        self.x = x\n",
    "        out = np.dot(x, self.W) + self.b\n",
    "        return out\n",
    "    def backward(self, dout):\n",
    "        dx = np.dot(dout, self.W.T)\n",
    "        self.dW = np.dot(self.x.T, dout)\n",
    "        self.db = np.sum(dout, axis=0)\n",
    "        return dx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.6.3、Softmax-with-Loss 层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "最后介绍一下输出层的softmax函数。  \n",
    "前面我们提到过，softmax函数会将输入值正规化之后再输出。  \n",
    "比如手写数字识别时， Softmax层的输出如图5-28所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_28.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在图5-28中， Softmax层将输入值正规化（将输出值的和调整为1）之后再输出。  \n",
    "另外，因为手写数字识别要进行10类分类，所以向Softmax层的输入也有10个。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "神经网络中进行的处理有**推理**（inference）和**学习**两个阶段。  \n",
    "神经网络的推理通常不使用Softmax层。  \n",
    "比如，用图5-28的网络进行推理时，会将最后一个 Affine层的输出作为识别结果。  \n",
    "神经网络中未被正规化的输出结果（图 5-28中 Softmax层前面的 Affine层的输出）有时\n",
    "被称为“得分”。  \n",
    "也就是说，当神经网络的推理只需要给出一个答案的情况下，因为此时只对得分最大值感兴趣，所以不需要Softmax层。  \n",
    "不过，神经网络的学习阶段则需要Softmax层。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面来实现Softmax层。考虑到这里也包含作为损失函数的交叉熵误差（cross entropy error），所以称为“Softmax-with-Loss层”。   \n",
    "Softmax-withLoss层（Softmax函数和交叉熵误差）的计算图如图5-29所示。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_29.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到， Softmax-with-Loss层有些复杂。  \n",
    "这里只给出了最终结果，对Softmax-with-Loss层的导出过程感兴趣的读者，请参照附录A。  \n",
    "\n",
    "图5-29的计算图可以简化成图5-30。  \n",
    "图5-30 的计算图中， softmax函数记为Softmax层，交叉熵误差记为Cross Entropy Error层。  \n",
    "这里假设要进行3类分类，从前面的层接收3个输入（得分）。  \n",
    "如图5-30所示， Softmax层将输入（a1, a2, a3）正规化，输出（y1,y2, y3）。   \n",
    "Cross Entropy Error层接收Softmax的输出（y1, y2, y3）和教师标签（t1,t2, t3），从这些数据中输出损失L。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./imgs/5_30.png\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图5-30中要注意的是反向传播的结果。   \n",
    "Softmax层的反向传播得到了（y1 − t1, y2 − t2, y3 − t3）这样“漂亮”的结果。  \n",
    "由于（y1, y2, y3）是Softmax层的输出，（t1, t2, t3）是监督数据，所以（y1 − t1, y2 − t2, y3 − t3）是Softmax层的输出和教师标签的差分。  \n",
    "神经网络的反向传播会把这个差分表示的误差传递给前面的层，这是神经网络学习中的重要性质。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "神经网络学习的目的就是通过调整权重参数，使神经网络的输出（Softmax的输出）接近教师标签。  \n",
    "因此，必须将神经网络的输出与教师标签的误差高效地传递给前面的层。  \n",
    "刚刚的（y1 − t1, y2 − t2, y3 − t3）正是Softmax层的输出与教师标签的差，直截了当地表示了当前神经网络的输出与教师标签的误差。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里考虑一个具体的例子，比如思考教师标签是（0, 1, 0），Softmax层的输出是(0.3, 0.2, 0.5)的情形。  \n",
    "因为正确解标签处的概率是0.2（20%），这个时候的神经网络未能进行正确的识别。  \n",
    "此时， Softmax层的反向传播传递的是(0.3, −0.8, 0.5)这样一个大的误差。  \n",
    "因为这个大的误差会向前面的层传播，所以Softmax层前面的层会从这个大的误差中学习到“大”的内容。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用交叉熵误差作为 softmax函数的损失函数后，反向传播得到（ y1 − t1, y2 − t2, y3 − t3）这样“漂亮”的结果。  \n",
    "实际上，这样“漂亮”  的结果并不是偶然的，而是为了得到这样的结果，特意设计了交叉熵误差函数。  \n",
    "回归问题中输出层使用“恒等函数”，损失函数使用“平方和误差”，也是出于同样的理由（3.5节）。  \n",
    "也就是说，使用“平方和误差”作为“恒等函数”的损失函数，反向传播才能得到（ y1 − t1, y2 − t2, y3 − t3）这样“漂亮”的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再举一个例子，比如思考教师标签是(0, 1, 0)， Softmax层的输出是(0.01, 0.99, 0)的情形（这个神经网络识别得相当准确）。  \n",
    "此时Softmax层的反向传播传递的是(0.01, −0.01, 0)这样一个小的误差。  \n",
    "这个小的误差也会向前面的层传播，因为误差很小，所以Softmax层前面的层学到的内容也很“小”。  \n",
    "现在来进行Softmax-with-Loss层的实现，实现过程如下所示。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SoftmaxWithLoss:\n",
    "    def __init__(self):\n",
    "        self.loss = None # 损失\n",
    "        self.y = None # softmax的输出\n",
    "        self.t = None # 监督数据（one-hot vector）\n",
    "    def forward(self, x, t):\n",
    "        self.t = t\n",
    "        self.y = softmax(x)\n",
    "        self.loss = cross_entropy_error(self.y, self.t)\n",
    "        return self.loss\n",
    "    def backward(self, dout=1):\n",
    "        batch_size = self.t.shape[0]\n",
    "        dx = (self.y - self.t) / batch_size\n",
    "        return dx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个实现利用了3.5.2节和4.2.4节中实现的 softmax()和 cross_entropy_error()函数。  \n",
    "因此，这里的实现非常简单。  \n",
    "请注意反向传播时，将要传播的值除以批的大小（batch_size）后，传递给前面的层的是单个数据的误差。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.7、误差反向传播法的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过像组装乐高积木一样组装上一节中实现的层，可以构建神经网络。  \n",
    "本节我们将通过组装已经实现的层来构建神经网络。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.7.1、神经网络学习的全貌图"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在进行具体的实现之前，我们再来确认一下神经网络学习的全貌图。神经网络学习的步骤如下所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 前提"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "神经网络中有合适的权重和偏置，调整权重和偏置以便拟合训练数据的过程称为学习。  \n",
    "神经网络的学习分为下面4个步骤："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤1（ mini-batch）  \n",
    "从训练数据中随机选择一部分数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤2（计算梯度）  \n",
    "计算损失函数关于各个权重参数的梯度。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤3（更新参数）  \n",
    "将权重参数沿梯度方向进行微小的更新。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 步骤4（重复）  \n",
    "重复步骤1、步骤2、步骤3。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "之前介绍的误差反向传播法会在步骤2中出现。  \n",
    "上一章中，我们利用数值微分求得了这个梯度。  \n",
    "数值微分虽然实现简单，但是计算要耗费较多的时间。  \n",
    "和需要花费较多时间的数值微分不同，误差反向传播法可以快速高效地计算梯度。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.7.2、对应误差反向传播法的神经网络的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在来进行神经网络的实现。  \n",
    "这里我们要把2层神经网络实现为TwoLayerNet。  \n",
    "首先，将这个类的实例变量和方法整理成表5-1和表5-2。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "表5-1　TwoLayerNet类的实例变量："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "实例变量|说明\n",
    "-|-\n",
    "params|保存神经网络的参数的字典型变量。params['W1']是第1层的权重， params['b1']是第1层的偏置。params['W2']是第2层的权重， params['b2']是第2层的偏置\n",
    "layers|保存神经网络的层的**有序字典型变量**。以layers['Affine1']、 layers['ReLu1']、 layers['Affine2']的形式，通过有序字典保存各个层\n",
    "lastLayer|神经网络的最后一层。本例中为SoftmaxWithLoss层"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "表5-2　TwoLayerNet类的方法："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "方法|说明\n",
    "-|-\n",
    "\\_\\_init__(self, input_size,hidden_size, output_size,weight_init_std)|进行初始化。参数从头开始依次是输入层的神经元数、隐藏层的神经元数、输出层的神经元数、初始化权重时的高斯分布的规模\n",
    "predict(self, x)|进行识别（推理）。参数x是图像数据\n",
    "loss(self, x, t)|计算损失函数的值。参数 X是图像数据、 t是正确解标签\n",
    "accuracy(self, x, t)|计算识别精度\n",
    "numerical_gradient(self, x, t)|通过数值微分计算关于权重参数的梯度（同上一章）\n",
    "gradient(self, x, t)|通过误差反向传播法计算关于权重参数的梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个类的实现稍微有一点长，但是内容和4.5节的学习算法的实现有很多共通的部分，不同点主要在于这里使用了层。  \n",
    "通过使用层，获得识别结果的处理（predict()）和计算梯度的处理（gradient()）只需通过层之间的传递就能完成。下面是TwoLayerNet的代码实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys, os\n",
    "sys.path.append(os.pardir)\n",
    "import numpy as np\n",
    "from common.layers import *\n",
    "from common.gradient import numerical_gradient\n",
    "from collections import OrderedDict\n",
    "class TwoLayerNet:\n",
    "    def __init__(self, input_size, hidden_size, output_size,\n",
    "        weight_init_std=0.01):\n",
    "        # 初始化权重\n",
    "        self.params = {}\n",
    "        self.params['W1'] = weight_init_std * \\\n",
    "        np.random.randn(input_size, hidden_size)\n",
    "        self.params['b1'] = np.zeros(hidden_size)\n",
    "        self.params['W2'] = weight_init_std * \\\n",
    "        np.random.randn(hidden_size, output_size)\n",
    "        self.params['b2'] = np.zeros(output_size)\n",
    "        \n",
    "        # 生成层\n",
    "        self.layers = OrderedDict()\n",
    "        self.layers['Affine1'] = \\\n",
    "        Affine(self.params['W1'], self.params['b1'])\n",
    "        self.layers['Relu1'] = Relu()\n",
    "        self.layers['Affine2'] = \\\n",
    "        Affine(self.params['W2'], self.params['b2'])\n",
    "        self.lastLayer = SoftmaxWithLoss()\n",
    "        \n",
    "    def predict(self, x):\n",
    "        for layer in self.layers.values():\n",
    "            x = layer.forward(x)\n",
    "        return x\n",
    "    \n",
    "    # x:输入数据, t:监督数据\n",
    "    def loss(self, x, t):\n",
    "        y = self.predict(x)\n",
    "        return self.lastLayer.forward(y, t)\n",
    "    \n",
    "    def accuracy(self, x, t):\n",
    "        y = self.predict(x)\n",
    "        y = np.argmax(y, axis=1)\n",
    "        if t.ndim != 1 : \n",
    "            t = np.argmax(t, axis=1)\n",
    "        accuracy = np.sum(y == t) / float(x.shape[0])\n",
    "        return accuracy\n",
    "        \n",
    "        \n",
    "    # x:输入数据, t:监督数据\n",
    "    def numerical_gradient(self, x, t):\n",
    "        loss_W = lambda W: self.loss(x, t)\n",
    "        grads = {}\n",
    "        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])\n",
    "        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])\n",
    "        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])\n",
    "        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])\n",
    "        return grads\n",
    "    \n",
    "    \n",
    "    def gradient(self, x, t):\n",
    "        # forward\n",
    "        self.loss(x, t)\n",
    "        # backward\n",
    "        dout = 1\n",
    "        dout = self.lastLayer.backward(dout)\n",
    "        layers = list(self.layers.values())\n",
    "        layers.reverse()\n",
    "        for layer in layers:\n",
    "            dout = layer.backward(dout)\n",
    "        # 设定\n",
    "        grads = {}\n",
    "        grads['W1'] = self.layers['Affine1'].dW\n",
    "        grads['b1'] = self.layers['Affine1'].db\n",
    "        grads['W2'] = self.layers['Affine2'].dW\n",
    "        grads['b2'] = self.layers['Affine2'].db\n",
    "        return grads"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "请注意这个实现中的粗体字代码部分，尤其是将神经网络的层保存为OrderedDict这一点非常重要。   \n",
    "OrderedDict是有序字典，“有序”是指它可以记住向字典里添加元素的顺序。  \n",
    "因此，神经网络的正向传播只需按照添加元素的顺序调用各层的 forward()方法就可以完成处理，而反向传播只需要按照相反的顺序调用各层即可。  \n",
    "因为Affine层和ReLU层的内部会正确处理正向传播和反向传播，所以这里要做的事情仅仅是以正确的顺序连接各层，再按顺序（或者逆序）调用各层。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "像这样通过将神经网络的组成元素以层的方式实现，可以轻松地构建神\n",
    "经网络。这个用层进行模块化的实现具有很大优点。  \n",
    "因为想另外构建一个神经网络（比如5层、 10层、 20层……的大的神经网络）时，只需像组装乐高\n",
    "积木那样添加必要的层就可以了。  \n",
    "之后，通过各个层内部实现的正向传播和反向传播，就可以正确计算进行识别处理或学习所需的梯度。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.7.3、误差反向传播法的梯度确认"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "到目前为止，我们介绍了两种求梯度的方法。  \n",
    "一种是基于数值微分的方法，另一种是解析性地求解数学式的方法。  \n",
    "后一种方法通过使用误差反向传播法，即使存在大量的参数，也可以高效地计算梯度。  \n",
    "因此，后文将不再使用耗费时间的数值微分，而是使用误差反向传播法求梯度。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "数值微分的计算很耗费时间，而且如果有误差反向传播法的（正确的）实现的话，就没有必要使用数值微分的实现了。  \n",
    "那么数值微分有什么用呢？  \n",
    "实际上，在确认误差反向传播法的实现是否正确时，是需要用到数值微分的。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "数值微分的优点是实现简单，因此，一般情况下不太容易出错。  \n",
    "而误差反向传播法的实现很复杂，容易出错。  \n",
    "所以，经常会比较数值微分的结果和误差反向传播法的结果，以确认误差反向传播法的实现是否正确。  \n",
    "确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致（严格地讲，是非常相近）的操作称为**梯度确认**（gradient check）。  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "W1:4.5831464488034006e-10\n",
      "b1:2.7971893137770237e-09\n",
      "W2:6.5103873027277756e-09\n",
      "b2:1.3948369523286708e-07\n"
     ]
    }
   ],
   "source": [
    "import sys, os\n",
    "sys.path.append(os.pardir)\n",
    "import numpy as np\n",
    "from dataset.mnist import load_mnist\n",
    "# from two_layer_net import TwoLayerNet\n",
    "\n",
    "# 读入数据\n",
    "(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label = True)\n",
    "\n",
    "network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)\n",
    "\n",
    "x_batch = x_train[:3]\n",
    "t_batch = t_train[:3]\n",
    "\n",
    "grad_numerical = network.numerical_gradient(x_batch, t_batch)\n",
    "grad_backprop = network.gradient(x_batch, t_batch)\n",
    "\n",
    "# 求各个权重的绝对误差的平均值\n",
    "for key in grad_numerical.keys():\n",
    "    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )\n",
    "    print(key + \":\" + str(diff))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和以前一样，读入MNIST数据集。然后，使用训练数据的一部分，确认数值微分求出的梯度和误差反向传播法求出的梯度的误差。  \n",
    "这里误差的计算方法是求各个权重参数中对应元素的差的绝对值，并计算其平均值。  \n",
    "运行上面的代码后，会输出如下结果。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从这个结果可以看出，通过数值微分和误差反向传播法求出的梯度的差非常小。  \n",
    "比如，第1层的偏置的误差是9.7e-13（0.00000000000097）。  \n",
    "这样一来，我们就知道了通过误差反向传播法求出的梯度是正确的，误差反向传播法的实现没有错误。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "数值微分和误差反向传播法的计算结果之间的误差为0是很少见的。  \n",
    "这是因为计算机的计算精度有限（比如，32位浮点数）。  \n",
    "受到数值精度的限制，刚才的误差一般不会为 0，但是如果实现正确的话，可以期待这个误差是一个接近0的很小的值。  \n",
    "如果这个值很大，就说明误差反向传播法的实现存在错误。  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.7.4、使用误差反向传播法的学习 "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "最后，我们来看一下使用了误差反向传播法的神经网络的学习的实现。  \n",
    "和之前的实现相比，不同之处仅在于通过误差反向传播法求梯度这一点。  \n",
    "这里只列出了代码，省略了说明  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.06875 0.0638\n",
      "0.90535 0.9095\n",
      "0.9231666666666667 0.9254\n",
      "0.93475 0.9345\n",
      "0.9437333333333333 0.9431\n",
      "0.9452333333333334 0.9426\n",
      "0.9552333333333334 0.9532\n",
      "0.9586833333333333 0.9552\n",
      "0.9622 0.9575\n",
      "0.9661833333333333 0.9608\n",
      "0.9674833333333334 0.9618\n",
      "0.9698666666666667 0.9644\n",
      "0.9705333333333334 0.9639\n",
      "0.97295 0.9643\n",
      "0.9736833333333333 0.9664\n",
      "0.9750166666666666 0.9675\n",
      "0.9782333333333333 0.9686\n"
     ]
    }
   ],
   "source": [
    "import sys, os\n",
    "sys.path.append(os.pardir)\n",
    "import numpy as np\n",
    "from dataset.mnist import load_mnist\n",
    "# from two_layer_net import TwoLayerNet\n",
    "\n",
    "# 读入数据\n",
    "(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)\n",
    "\n",
    "network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)\n",
    "\n",
    "iters_num = 10000\n",
    "train_size = x_train.shape[0]\n",
    "batch_size = 100\n",
    "learning_rate = 0.1\n",
    "train_loss_list = []\n",
    "train_acc_list = []\n",
    "test_acc_list = []\n",
    "\n",
    "iter_per_epoch = max(train_size / batch_size, 1)\n",
    "\n",
    "for i in range(iters_num):\n",
    "    batch_mask = np.random.choice(train_size, batch_size)\n",
    "    x_batch = x_train[batch_mask]\n",
    "    t_batch = t_train[batch_mask]\n",
    "    \n",
    "    # 通过误差反向传播法求梯度\n",
    "    grad = network.gradient(x_batch, t_batch)\n",
    "    \n",
    "    # 更新\n",
    "    for key in ('W1', 'b1', 'W2', 'b2'):\n",
    "        network.params[key] -= learning_rate * grad[key]\n",
    "        \n",
    "    loss = network.loss(x_batch, t_batch)\n",
    "    train_loss_list.append(loss)\n",
    "    \n",
    "    if i % iter_per_epoch == 0:\n",
    "        train_acc = network.accuracy(x_train, t_train)\n",
    "        test_acc = network.accuracy(x_test, t_test)\n",
    "        train_acc_list.append(train_acc)\n",
    "        test_acc_list.append(test_acc)\n",
    "        print(train_acc, test_acc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.8、小结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本章我们介绍了将计算过程可视化的计算图，并使用计算图，介绍了神经网络中的误差反向传播法，并以层为单位实现了神经网络中的处理。  \n",
    "我们学过的层有ReLU层、 Softmax-with-Loss层、 Affine层、 Softmax层等，这些层中实现了 forward和 backward方法，通过将数据正向和反向地传播，可以高效地计算权重参数的梯度。通过使用层进行模块化，神经网络中可以自由地组装层，轻松构建出自己喜欢的网络。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "1、通过使用计算图，可以直观地把握计算过程。\n",
    "2、计算图的节点是由局部计算构成的。局部计算构成全局计算。\n",
    "3、计算图的正向传播进行一般的计算。通过计算图的反向传播，可以计算各个节点的导数。\n",
    "4、通过将神经网络的组成元素实现为层，可以高效地计算梯度（反向传播法）。\n",
    "5、通过比较数值微分和误差反向传播法的结果，可以确认误差反向传播法的实现是否正确（梯度确认）。\n",
    "'''"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
